{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "c3e2458f-d038-4845-93a0-d4ad830f9f90",
   "metadata": {},
   "source": [
    "# LangChain 核心模块学习：Chains\n",
    "\n",
    "对于简单的大模型应用，单独使用语言模型（LLMs）是可以的。\n",
    "\n",
    "**但更复杂的大模型应用需要将 `LLMs` 和 `Chat Models` 链接在一起 - 要么彼此链接，要么与其他组件链接。**\n",
    "\n",
    "LangChain 为这种“链式”应用程序提供了 `Chain` 接口。\n",
    "\n",
    "LangChain 以通用方式定义了 `Chain`，它是对组件进行调用序列的集合，其中可以包含其他链。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "24efe291-8745-4e78-bdb4-648e60d7bac8",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Defaulting to user installation because normal site-packages is not writeable\n",
      "Requirement already satisfied: langchain in c:\\users\\lenovo\\appdata\\roaming\\python\\python310\\site-packages (0.2.7)\n",
      "Requirement already satisfied: PyYAML>=5.3 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from langchain) (6.0.1)\n",
      "Requirement already satisfied: SQLAlchemy<3,>=1.4 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from langchain) (2.0.31)\n",
      "Requirement already satisfied: aiohttp<4.0.0,>=3.8.3 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from langchain) (3.9.5)\n",
      "Requirement already satisfied: async-timeout<5.0.0,>=4.0.0 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from langchain) (4.0.3)\n",
      "Requirement already satisfied: langchain-core<0.3.0,>=0.2.12 in c:\\users\\lenovo\\appdata\\roaming\\python\\python310\\site-packages (from langchain) (0.2.13)\n",
      "Requirement already satisfied: langchain-text-splitters<0.3.0,>=0.2.0 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from langchain) (0.2.0)\n",
      "Requirement already satisfied: langsmith<0.2.0,>=0.1.17 in c:\\users\\lenovo\\appdata\\roaming\\python\\python310\\site-packages (from langchain) (0.1.85)\n",
      "Requirement already satisfied: numpy<2,>=1 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from langchain) (1.26.4)\n",
      "Requirement already satisfied: pydantic<3,>=1 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from langchain) (2.5.3)\n",
      "Requirement already satisfied: requests<3,>=2 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from langchain) (2.31.0)\n",
      "Requirement already satisfied: tenacity!=8.4.0,<9.0.0,>=8.1.0 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from langchain) (8.4.2)\n",
      "Requirement already satisfied: aiosignal>=1.1.2 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (1.3.1)\n",
      "Requirement already satisfied: attrs>=17.3.0 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (23.2.0)\n",
      "Requirement already satisfied: frozenlist>=1.1.1 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (1.4.1)\n",
      "Requirement already satisfied: multidict<7.0,>=4.5 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (6.0.5)\n",
      "Requirement already satisfied: yarl<2.0,>=1.0 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (1.9.4)\n",
      "Requirement already satisfied: jsonpatch<2.0,>=1.33 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from langchain-core<0.3.0,>=0.2.12->langchain) (1.33)\n",
      "Requirement already satisfied: packaging<25,>=23.2 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from langchain-core<0.3.0,>=0.2.12->langchain) (23.2)\n",
      "Requirement already satisfied: orjson<4.0.0,>=3.9.14 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from langsmith<0.2.0,>=0.1.17->langchain) (3.10.5)\n",
      "Requirement already satisfied: annotated-types>=0.4.0 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from pydantic<3,>=1->langchain) (0.6.0)\n",
      "Requirement already satisfied: pydantic-core==2.14.6 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from pydantic<3,>=1->langchain) (2.14.6)\n",
      "Requirement already satisfied: typing-extensions>=4.6.1 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from pydantic<3,>=1->langchain) (4.12.2)\n",
      "Requirement already satisfied: charset-normalizer<4,>=2 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from requests<3,>=2->langchain) (3.3.2)\n",
      "Requirement already satisfied: idna<4,>=2.5 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from requests<3,>=2->langchain) (3.6)\n",
      "Requirement already satisfied: urllib3<3,>=1.21.1 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from requests<3,>=2->langchain) (2.2.2)\n",
      "Requirement already satisfied: certifi>=2017.4.17 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from requests<3,>=2->langchain) (2024.2.2)\n",
      "Requirement already satisfied: greenlet!=0.4.17 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from SQLAlchemy<3,>=1.4->langchain) (3.0.3)\n",
      "Requirement already satisfied: jsonpointer>=1.9 in c:\\programdata\\anaconda3\\envs\\langchain\\lib\\site-packages (from jsonpatch<2.0,>=1.33->langchain-core<0.3.0,>=0.2.12->langchain) (3.0.0)\n"
     ]
    }
   ],
   "source": [
    "! pip install -U langchain"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c81a7df0-26c7-4eb8-92f1-cc54445cf507",
   "metadata": {},
   "source": [
    "## LLMChain\n",
    "\n",
    "LLMChain 是 LangChain 中最简单的链，作为其他复杂 Chains 和 Agents 的内部调用，被广泛应用。\n",
    "\n",
    "一个LLMChain由PromptTemplate和语言模型（LLM or Chat Model）组成。它使用直接传入（或 memory 提供）的 key-value 来规范化生成 Prompt Template（提示模板），并将生成的 prompt （格式化后的字符串）传递给大模型，并返回大模型输出。\n",
    "\n",
    "![](../images/llm_chain.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4fbd5ca7-ca54-4701-919c-2857266caefc",
   "metadata": {},
   "source": [
    "## Router Chain: 实现条件判断的大模型调用\n",
    "\n",
    "\n",
    "这段代码构建了一个可定制的链路系统，用户可以提供不同的输入提示，并根据这些提示获取适当的响应。\n",
    "\n",
    "主要逻辑：从`prompt_infos`创建多个`LLMChain`对象，并将它们保存在一个字典中，然后创建一个默认的`ConversationChain`，最后创建一个带有路由功能的`MultiPromptChain`。\n",
    "\n",
    "![](../images/router_chain.png)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "aaf8c391-9225-4e66-ad4d-d689b53a0379",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.chains.router import MultiPromptChain\n",
    "from langchain_openai import OpenAI\n",
    "from langchain.chains import ConversationChain\n",
    "from langchain.chains.llm import LLMChain\n",
    "from langchain.prompts import PromptTemplate"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "33b5061c-391e-4762-91c7-73b57f4ab501",
   "metadata": {},
   "outputs": [],
   "source": [
    "physics_template = \"\"\"你是一位非常聪明的物理教授。\n",
    "你擅长以简洁易懂的方式回答关于物理的问题。\n",
    "当你不知道某个问题的答案时，你会坦诚承认。\n",
    "\n",
    "这是一个问题：\n",
    "{input}\"\"\"\n",
    "\n",
    "\n",
    "math_template = \"\"\"你是一位很棒的数学家。你擅长回答数学问题。\n",
    "之所以如此出色，是因为你能够将难题分解成各个组成部分，\n",
    "先回答这些组成部分，然后再将它们整合起来回答更广泛的问题。\n",
    "\n",
    "这是一个问题：\n",
    "{input}\"\"\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "5ef1db6e-3da4-4f9b-9707-0f30aa293dd7",
   "metadata": {},
   "outputs": [],
   "source": [
    "prompt_infos = [\n",
    "    {\n",
    "        \"name\": \"物理\",\n",
    "        \"description\": \"适用于回答物理问题\",\n",
    "        \"prompt_template\": physics_template,\n",
    "    },\n",
    "    {\n",
    "        \"name\": \"数学\",\n",
    "        \"description\": \"适用于回答数学问题\",\n",
    "        \"prompt_template\": math_template,\n",
    "    },\n",
    "]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "3983cafe-c2d5-4951-b779-88d844594777",
   "metadata": {},
   "outputs": [],
   "source": [
    "llm = OpenAI(model_name=\"gpt-3.5-turbo-instruct\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "db8be9f0-1ac2-4ded-8950-6403cfa40004",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "C:\\Users\\lenovo\\AppData\\Roaming\\Python\\Python310\\site-packages\\langchain_core\\_api\\deprecation.py:139: LangChainDeprecationWarning: The class `LLMChain` was deprecated in LangChain 0.1.17 and will be removed in 1.0. Use RunnableSequence, e.g., `prompt | llm` instead.\n",
      "  warn_deprecated(\n",
      "C:\\Users\\lenovo\\AppData\\Roaming\\Python\\Python310\\site-packages\\langchain_core\\_api\\deprecation.py:139: LangChainDeprecationWarning: The class `ConversationChain` was deprecated in LangChain 0.2.7 and will be removed in 1.0. Use RunnableWithMessageHistory: https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.history.RunnableWithMessageHistory.html instead.\n",
      "  warn_deprecated(\n"
     ]
    }
   ],
   "source": [
    "# 创建一个空的目标链字典，用于存放根据prompt_infos生成的LLMChain。\n",
    "destination_chains = {}\n",
    "\n",
    "# 遍历prompt_infos列表，为每个信息创建一个LLMChain。\n",
    "for p_info in prompt_infos:\n",
    "    name = p_info[\"name\"]  # 提取名称\n",
    "    prompt_template = p_info[\"prompt_template\"]  # 提取模板\n",
    "    # 创建PromptTemplate对象\n",
    "    prompt = PromptTemplate(template=prompt_template, input_variables=[\"input\"])\n",
    "    # 使用上述模板和llm对象创建LLMChain对象\n",
    "    chain = LLMChain(llm=llm, prompt=prompt)\n",
    "    # 将新创建的chain对象添加到destination_chains字典中\n",
    "    destination_chains[name] = chain\n",
    "\n",
    "# 创建一个默认的ConversationChain\n",
    "default_chain = ConversationChain(llm=llm, output_key=\"text\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "ae77b13a-2077-4e80-83f9-a2b1d8398461",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "langchain.chains.conversation.base.ConversationChain"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "type(default_chain)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f5aa4a82-2d96-4124-8896-4e11e5d5c8e9",
   "metadata": {},
   "source": [
    "### 使用 LLMRouterChain 实现条件判断调用\n",
    "\n",
    "这段代码定义了一个chain对象（LLMRouterChain），该对象首先使用router_chain来决定哪个destination_chain应该被执行，如果没有合适的目标链，则默认使用default_chain。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "1c196e6c-e767-4d4f-8327-50ead641bc3a",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.chains.router.llm_router import LLMRouterChain, RouterOutputParser\n",
    "from langchain.chains.router.multi_prompt_prompt import MULTI_PROMPT_ROUTER_TEMPLATE"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "f5ada86e-e430-412c-828d-b053b630f07c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 从prompt_infos中提取目标信息并将其转化为字符串列表\n",
    "destinations = [f\"{p['name']}: {p['description']}\" for p in prompt_infos]\n",
    "# 使用join方法将列表转化为字符串，每个元素之间用换行符分隔\n",
    "destinations_str = \"\\n\".join(destinations)\n",
    "# 根据MULTI_PROMPT_ROUTER_TEMPLATE格式化字符串和destinations_str创建路由模板\n",
    "router_template = MULTI_PROMPT_ROUTER_TEMPLATE.format(destinations=destinations_str)\n",
    "# 创建路由的PromptTemplate\n",
    "router_prompt = PromptTemplate(\n",
    "    template=router_template,\n",
    "    input_variables=[\"input\"],\n",
    "    output_parser=RouterOutputParser(),\n",
    ")\n",
    "# 使用上述路由模板和llm对象创建LLMRouterChain对象\n",
    "router_chain = LLMRouterChain.from_llm(llm, router_prompt)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "8c1013dc-ae1f-468d-96b3-4babe0d50d1f",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['物理: 适用于回答物理问题', '数学: 适用于回答数学问题']\n"
     ]
    }
   ],
   "source": [
    "print(destinations)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "a85ef126-aca1-40c2-8e01-d15af5500785",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "物理: 适用于回答物理问题\n",
      "数学: 适用于回答数学问题\n"
     ]
    }
   ],
   "source": [
    "print(destinations_str)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "5db81fcb-704a-4250-a6b5-210e4be77af5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Given a raw text input to a language model select the model prompt best suited for the input. You will be given the names of the available prompts and a description of what the prompt is best suited for. You may also revise the original input if you think that revising it will ultimately lead to a better response from the language model.\n",
      "\n",
      "<< FORMATTING >>\n",
      "Return a markdown code snippet with a JSON object formatted to look like:\n",
      "```json\n",
      "{{{{\n",
      "    \"destination\": string \\ name of the prompt to use or \"DEFAULT\"\n",
      "    \"next_inputs\": string \\ a potentially modified version of the original input\n",
      "}}}}\n",
      "```\n",
      "\n",
      "REMEMBER: \"destination\" MUST be one of the candidate prompt names specified below OR it can be \"DEFAULT\" if the input is not well suited for any of the candidate prompts.\n",
      "REMEMBER: \"next_inputs\" can just be the original input if you don't think any modifications are needed.\n",
      "\n",
      "<< CANDIDATE PROMPTS >>\n",
      "{destinations}\n",
      "\n",
      "<< INPUT >>\n",
      "{{input}}\n",
      "\n",
      "<< OUTPUT (must include ```json at the start of the response) >>\n",
      "<< OUTPUT (must end with ```) >>\n",
      "\n"
     ]
    }
   ],
   "source": [
    "print(MULTI_PROMPT_ROUTER_TEMPLATE)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "f882244c-1fa6-4d74-a44c-578c9fb25e18",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Given a raw text input to a language model select the model prompt best suited for the input. You will be given the names of the available prompts and a description of what the prompt is best suited for. You may also revise the original input if you think that revising it will ultimately lead to a better response from the language model.\n",
      "\n",
      "<< FORMATTING >>\n",
      "Return a markdown code snippet with a JSON object formatted to look like:\n",
      "```json\n",
      "{{\n",
      "    \"destination\": string \\ name of the prompt to use or \"DEFAULT\"\n",
      "    \"next_inputs\": string \\ a potentially modified version of the original input\n",
      "}}\n",
      "```\n",
      "\n",
      "REMEMBER: \"destination\" MUST be one of the candidate prompt names specified below OR it can be \"DEFAULT\" if the input is not well suited for any of the candidate prompts.\n",
      "REMEMBER: \"next_inputs\" can just be the original input if you don't think any modifications are needed.\n",
      "\n",
      "<< CANDIDATE PROMPTS >>\n",
      "物理: 适用于回答物理问题\n",
      "数学: 适用于回答数学问题\n",
      "\n",
      "<< INPUT >>\n",
      "{input}\n",
      "\n",
      "<< OUTPUT (must include ```json at the start of the response) >>\n",
      "<< OUTPUT (must end with ```) >>\n",
      "\n"
     ]
    }
   ],
   "source": [
    "print(router_template)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "c2a482e4-5757-4295-a3d8-c3fdd1d4abd2",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 创建MultiPromptChain对象，其中包含了路由链，目标链和默认链。\n",
    "chain = MultiPromptChain(\n",
    "    router_chain=router_chain,\n",
    "    destination_chains=destination_chains,\n",
    "    default_chain=default_chain,\n",
    "    verbose=True,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "128bb7a0-b176-4b14-835e-8aaa723ab441",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "\n",
      "\u001b[1m> Entering new MultiPromptChain chain...\u001b[0m\n",
      "物理: {'input': 'What is blackbody radiation?'}\n",
      "\u001b[1m> Finished chain.\u001b[0m\n",
      "{'input': 'What is blackbody radiation?', 'text': '\\n\\n黑体辐射是一种物理现象，指的是由于物体内部的热运动导致的电磁辐射。简单来说，物体在受热时会发出电磁波，这些波的频率和强度取决于物体的温度。黑体辐射的特点是它的辐射频率与物体的温度无关，而只与物体的结构和性质有关。这个概念在热力学和量子力学中都有重要的应用。'}\n"
     ]
    }
   ],
   "source": [
    "print(chain.invoke(\"黑体辐射是什么？?\"))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "cd869807-9cec-4bb2-9104-ecc4efce9baa",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "\n",
      "\u001b[1m> Entering new MultiPromptChain chain...\u001b[0m\n",
      "数学: {'input': '大于40的第一个质数是多少，使得这个质数加一能被3整除？'}\n",
      "\u001b[1m> Finished chain.\u001b[0m\n",
      "{'input': '大于40的第一个质数是多少，使得这个质数加一能被3整除？', 'text': '\\n\\n首先，我们需要确定大于40的质数。40不是质数，我们从41开始检查。41是质数，但41+1=42不能被3整除。接下来是43，43+1=44也不能被3整除。然后是47，47+1=48可以被3整除。所以大于40的第一个质数是47，47+1=48可以被3整除。'}\n"
     ]
    }
   ],
   "source": [
    "print(\n",
    "    chain.invoke(\n",
    "        \"大于40的第一个质数是多少，使得这个质数加一能被3整除？\"\n",
    "    )\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "7ad5dcb2-48c0-4d0f-b6cc-09ebcbdce75e",
   "metadata": {},
   "outputs": [],
   "source": [
    "router_chain.verbose = True"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "bd37e004-bb24-4929-992c-34407593d86e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "\n",
      "\u001b[1m> Entering new MultiPromptChain chain...\u001b[0m\n",
      "\n",
      "\n",
      "\u001b[1m> Entering new LLMRouterChain chain...\u001b[0m\n",
      "\n",
      "\u001b[1m> Finished chain.\u001b[0m\n",
      "物理: {'input': '黑洞是什么？'}\n",
      "\u001b[1m> Finished chain.\u001b[0m\n",
      "{'input': '黑洞是什么？', 'text': '\\n\\n黑洞是一种极端密度和强大引力的天体，它的重力场非常强大，甚至连光也无法逃脱。它的存在是由于恒星在死亡时发生了坍缩，使得它们的质量集中在极小的空间内。黑洞的存在已经被多项观测和理论证据所证实，但仍然有很多未解之谜。'}\n"
     ]
    }
   ],
   "source": [
    "print(chain.invoke(\"黑洞是什么？\"))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a51119ed-025f-48d7-ad81-cd9cdab7090f",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cbda2930-a0e6-48b2-8e02-4c3d792f0225",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "24d11e0f-d5ee-4086-9e1a-b21000232134",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "8b6836f0-213d-4cac-abc9-3617831be3db",
   "metadata": {},
   "source": [
    "### Homework\n",
    "\n",
    "#### 扩展 Demo：实现生物、计算机和汉语言文学老师 PromptTemplates 及对应 Chains"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1c7edb0a-675d-40c0-9f5d-d58f0170ce72",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
